package model.game2048;

import java.io.BufferedReader;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.PrintWriter;
import java.io.Serializable;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.Observable;
import java.util.Random;

import audio.AudioPlayer;

import com.thoughtworks.xstream.XStream;

import controller.server.Message;
import model.Model;
import model.game2048.State;
/**
 * The Game2048Model class is one layer out of three of the MVP design pattern.This layer (the model layer) 
 * holds the implementation of the logic and the calculation.
 * 
 * @author Peled Eldan
 * @author Daniel Manor
 */
public class Game2048Model extends Observable implements Model, Serializable
{
	// SerializationVersionUID	
	private static final long serialVersionUID = 302986062L;

	// Data Members
	private int[][] board;
	private int N;
	private ArrayList<String> emptySlots;
	private int score;
	private LinkedList<int[][]> boardUndoLinkedList;
	private LinkedList<Integer> scoreUndoLinkedList;
	private LinkedList<ArrayList<String>> emptySlotsUndoLinkedList;
	private XStream xstream;
	private AudioPlayer bgMusic;
	private int muteFlag;

	// Statics
	private static final int EMPTY = 0;
	private static final String SEPARATOR = ",";
	private static final int DIRECTION_UP = 1;
	private static final int DIRECTION_DOWN = -1;
	private static final int DIRECTION_LEFT = 1;
	private static final int DIRECTION_RIGHT = -1;

	// Game2048Model CTOR
	public Game2048Model()
	{
		this.N = 4;
		this.bgMusic = new AudioPlayer("/sounds/BGMusic.mp3");
		this.board = new int[N][N];

		for (int i = 0; i < this.N; i++)
			for (int j = 0; j < this.N; j++)
				this.board[i][j] = Game2048Model.EMPTY;


		this.emptySlots = new ArrayList<String>();
		initializeEmptySlots();

		this.boardUndoLinkedList = new LinkedList<int[][]>();
		this.scoreUndoLinkedList = new LinkedList<Integer>();
		this.emptySlotsUndoLinkedList = new LinkedList<ArrayList<String>>();

		this.score = 0;

		xstream = new XStream();
		xstream.alias("Game2048Model", Game2048Model.class);
		xstream.omitField(Game2048Model.class, "xstream");
		
		setChanged();
		notifyObservers();
	}

	// Fill the emptySlots ArrayList with every slot on the board
	private void initializeEmptySlots()
	{
		for (int i = 0; i < this.N; i++)
			for (int j = 0; j < this.N; j++)
				emptySlots.add(parseSlotToString(new int[]{i,j}));
	}

	// Generate a slot string from an int array (size 2)
	private String parseSlotToString(int[] intArr)
	{
		String str = new String();

		str += (Integer.toString(intArr[0]));
		str += (Game2048Model.SEPARATOR);
		str += (Integer.toString(intArr[1]));

		return str;
	}

	// Generate a slot integer array from a string
	private int[] parseSlotToIntArr(String str)
	{
		String[] splitStr = str.split(Game2048Model.SEPARATOR);
		int[] splitInt = new int[2];

		splitInt[0] = Integer.parseInt(splitStr[0]);
		splitInt[1] = Integer.parseInt(splitStr[1]);

		return splitInt;
	}

	// Get a random empty slot from the emptySlots ArrayList
	private int[] getRandomEmptySlot()
	{
		int maxNum = this.emptySlots.size()-1;
		int minNum = 0;

		Random random = new Random();
		int randomNum = random.nextInt((maxNum - minNum) + 1) + minNum;

		return parseSlotToIntArr(this.emptySlots.get(randomNum));
	}

	// Randomize whether to put a two or a four as the new slot
	private int randomNum()
	{
		int maxNum = 10;
		int minNum = 1;

		Random random = new Random();
		int randomNum = random.nextInt((maxNum - minNum) + 1) + minNum;

		if (randomNum == 4)
			return 4;
		else
			return 2;
	}

	// Add a random number (TwoOrFour method) at a random slot (getRandomEmptySlot method)
	private void addRandomNumAtRandomSlot()
	{
		// Get a random empty slot
		int[] emptySlot = getRandomEmptySlot();

		// Add a Two or a Four to the empty Slot
		this.board[emptySlot[0]][emptySlot[1]] = randomNum();

		// Remove the slot from the emptySlots ArrayList
		this.emptySlots.remove(parseSlotToString(emptySlot));
	}

	@Override
	// Move every slot upwards.
	// Move each column separately using the moveEntireCol method.
	// After that, addRandomNumAtRandomSlot.
	// Lastly, notifyAllObservers.
	public void moveUp() 
	{
		// TODO integrate isGameOver into the game somehow

		boolean moved = false;
		boolean tempMoved = false;

		addCurrentBoardToUndo();

		for (int i = 0; i < this.N; i++)
		{
			tempMoved = moveEntireCol(i, Game2048Model.DIRECTION_UP);
			moved = tempMoved || moved;
		}

		if (moved == true)
		{
			// Add a new number to the board
			addRandomNumAtRandomSlot();

			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made
			notifyObservers();
		}
		else if (isGameOver())
		{
			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made, with a false value meaning game over
			notifyObservers(false);
		}
	}

	@Override
	// Move every slot downwards.
	// Move each column separately using the moveEntireCol method.
	// After that, addRandomNumAtRandomSlot.
	// Lastly, notifyAllObservers.
	public void moveDown() 
	{
		boolean moved = false;
		boolean tempMoved = false;

		addCurrentBoardToUndo();

		for (int i = 0; i < this.N; i++)
		{
			tempMoved = moveEntireCol(i, Game2048Model.DIRECTION_DOWN);
			moved = tempMoved || moved;
		}

		if (moved == true)
		{
			// Add a new number to the board
			addRandomNumAtRandomSlot();

			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made
			notifyObservers();
		}
		else if (isGameOver())
		{
			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made, with a false value meaning game over
			notifyObservers(false);
		}
	}

	// Move an entire column up / down
	private boolean moveEntireCol(int col, int direction)
	{		
		// Initialize local method variables
		int currRow = 1;
		boolean moved = false;
		ArrayList<String> mergedAlready = new ArrayList<String>();

		// Set currRow variable according to the direction
		if (direction == Game2048Model.DIRECTION_UP)
			currRow = 1;
		else if (direction == Game2048Model.DIRECTION_DOWN)
			currRow = this.N -2;

		// Move according to the direction
		while ((currRow < this.N) && (currRow >= 0))
		{
			// Initialize the currSlot and slotToCompare variables
			int currSlot = this.board[currRow][col];
			int slotToCompare = this.board[currRow + (-1 * direction)][col];

			// if currSlot is empty -> continue
			if (currSlot == Game2048Model.EMPTY)
			{
				currRow += direction;
				continue;
			}
			// If currSlot == slotToCompare and slotToCompare hasn't been merged yet
			else if ((currSlot == slotToCompare) && (!mergedAlready.contains(parseSlotToString(new int[] {currRow + (-1 * direction),col}))))
			{
				// Sum the two slots
				this.board[currRow + (-1 * direction)][col] = currSlot + slotToCompare;

				// Mark the spare slot as empty
				this.board[currRow][col] = Game2048Model.EMPTY;
				this.emptySlots.add(parseSlotToString(new int[]{currRow, col}));

				// Add to score
				addScore(this.board[currRow + (-1 * direction)][col]);

				// Mark the slot as mergedAlready
				mergedAlready.add(parseSlotToString(new int[] {currRow + (-1 * direction),col}));

				// Mark the moved flag
				moved = true;

				// Keep going
				currRow += direction;
			}
			else // currSlot != slotToCompare
			{
				if (this.emptySlots.contains(parseSlotToString(new int[]{currRow + (-1 * direction), col}))) // slotToCompare is empty
				{
					// Place currSlot value in the slotToCompare
					this.board[currRow + (-1 * direction)][col] = currSlot;

					// Remove slotToCompare from emptySlots ArrayList
					this.emptySlots.remove(parseSlotToString(new int[]{currRow + (-1 * direction), col}));

					// Mark the spare slot as empty
					this.board[currRow][col] = Game2048Model.EMPTY;
					this.emptySlots.add(parseSlotToString(new int[]{currRow, col}));

					// Keep going up/down
					if ((currRow + (-1 * direction) != 0) && (currRow + (-1 * direction) != this.N-1))
						currRow -= direction;
					else
						currRow += direction;

					// Mark the moved flag
					moved = true;
				}
				else // slotToCompare is NOT empty && currSlot != slotToCompare
				{
					// Keep going
					currRow += direction;
				}
			}
		}

		return moved;
	}

	@Override
	// Move every slot left.
	// Move each column separately using the moveEntireRow method.
	// After that, addRandomNumAtRandomSlot.
	// Lastly, notifyAllObservers.
	public void moveLeft() 
	{
		boolean moved = false;
		boolean tempMoved = false;

		addCurrentBoardToUndo();

		for (int i = 0; i < this.N; i++)
		{
			tempMoved = moveEntireRow(i, Game2048Model.DIRECTION_LEFT);
			moved = tempMoved || moved;
		}

		if (moved == true)
		{
			// Add a new number to the board
			addRandomNumAtRandomSlot();

			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made
			notifyObservers();
		}
		else if (isGameOver())
		{
			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made, with a false value meaning game over
			notifyObservers(false);
		}
	}

	@Override
	// Move every slot right.
	// Move each column separately using the moveEntireRow method.
	// After that, addRandomNumAtRandomSlot.
	// Lastly, notifyAllObservers.
	public void moveRight() 
	{
		boolean moved = false;
		boolean tempMoved = false;

		addCurrentBoardToUndo();

		for (int i = 0; i < this.N; i++)
		{
			tempMoved = moveEntireRow(i, Game2048Model.DIRECTION_RIGHT);
			moved = tempMoved || moved;
		}

		if (moved == true)
		{
			// Add a new number to the board
			addRandomNumAtRandomSlot();

			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made
			notifyObservers();
		}
		else if (isGameOver())
		{
			// raise a flag of a change
			setChanged();

			// Notify all of the observers that a change has been made, with a false value meaning game over
			notifyObservers(false);
		}
	}

	// Move an entire row to the left / right
	private boolean moveEntireRow(int row, int direction)
	{
		// Initialize local method variables
		int currCol = 1;
		boolean moved = false;
		ArrayList<String> mergedAlready = new ArrayList<String>();

		// Set currCol variable according to the direction
		if (direction == Game2048Model.DIRECTION_LEFT)
			currCol = 1;
		else if (direction == Game2048Model.DIRECTION_RIGHT)
			currCol = this.N -2;

		// Move according to the direction
		while ((currCol < this.N) && (currCol >= 0))
		{
			// Initialize the currSlot and slotToCompare variables			
			int currSlot = this.board[row][currCol];
			int slotToCompare = this.board[row][currCol + (-1 * direction)];

			// if currSlot is empty -> continue			
			if (currSlot == Game2048Model.EMPTY)
			{
				currCol += direction;
				continue;
			}
			// If currSlot == slotToCompare and slotToCompare hasn't been merged yet
			else if ((currSlot == slotToCompare) && (!mergedAlready.contains(parseSlotToString(new int[] {row, currCol + (-1 * direction)}))))
			{
				// Sum the two slots
				this.board[row][currCol + (-1 * direction)] = currSlot + slotToCompare;

				// Mark the spare as empty
				this.board[row][currCol] = Game2048Model.EMPTY;
				this.emptySlots.add(parseSlotToString(new int[] {row, currCol}));

				// Add to score
				addScore(this.board[row][currCol + (-1 * direction)]);

				// Mark the slot as mergedAlready
				mergedAlready.add(parseSlotToString(new int[] {row, currCol + (-1 * direction)}));

				// Mark the moved flag
				moved = true;

				// Keep going
				currCol += direction;
			}
			else // currSlot != slotToCompare
			{
				if (this.emptySlots.contains(parseSlotToString(new int[] {row, currCol + (-1 * direction)}))) // slotToCompare is empty
				{
					// Place currSlot value in the slotToCompare
					this.board[row][currCol + (-1 * direction)] = currSlot;

					// Remove slotToCompare from emptySlots ArrayList
					this.emptySlots.remove(parseSlotToString(new int[] {row, currCol + (-1 * direction)}));

					// Mark the spare slot as empty
					this.board[row][currCol] = Game2048Model.EMPTY;
					this.emptySlots.add(parseSlotToString(new int[] {row, currCol}));

					// Keep going left/right
					if ((currCol + (-1 * direction) != 0) && (currCol + (-1 * direction) != this.N-1))
						currCol -= direction;
					else
						currCol += direction;

					// Mark the moved flag
					moved = true;
				}
				else // slotToCompare is NOT empty && currSlot != slotToCompare
				{
					currCol += direction;
				}
			}
		}
		return moved;
	}

	// Check whether the game is over
	public boolean isGameOver()
	{
		if (!emptySlots.isEmpty())
			return false;

		for (int i = 0; i < this.N-1; i++)
		{
			for (int j = 0; j < this.N-1; j++)
			{ 
				if ((this.board[i][j] == this.board[i+1][j]) || (this.board[i][j] == this.board[i][j+1]))
					return false;
			}
		}

		for (int j = 0; j < this.N-1; j++)
		{
			if (this.board[this.N-1][j] == this.board[this.N-1][j+1])
				return false;
		}

		for (int i = 0; i < this.N-1; i++)
		{
			if (this.board[i][this.N-1] == this.board[i+1][this.N-1])
				return false;
		}
		return true;
	}

	@Override
	// Get the board integer 2D-array
	public int[][] getData() 
	{
		return this.board;
	}

	// Get the score value
	public int getScore()
	{
		return this.score;
	}

	// Add an integer to the score
	private int addScore(int toAdd)
	{
		this.score += toAdd;
		return this.score;
	}

	@Override
	// Start the game
	public void startGame() 
	{
		//BG Music
		if(muteFlag == 1)
		{
			bgMusic.stop();
		}
		else
		{
			bgMusic.play();	
		}
		
		this.score = 0;

		this.board = new int[4][4];

		for (int i = 0; i < this.N; i++)
			for (int j = 0; j < this.N; j++)
				this.board[i][j] = Game2048Model.EMPTY;

		this.emptySlots.clear();
		initializeEmptySlots();

		clearUndoLinkedLists();


		addRandomNumAtRandomSlot();
		addRandomNumAtRandomSlot();

		setChanged();
		notifyObservers();
	}

	private void addCurrentBoardToUndo() 
	{
		int[][] tempBoard = new int[this.N][this.N];
		for (int i = 0; i < this.N; i++)
			tempBoard[i] = Arrays.copyOf(this.board[i], this.board.length);
		this.boardUndoLinkedList.addFirst(tempBoard);

		this.scoreUndoLinkedList.addFirst(this.score);		
		this.emptySlotsUndoLinkedList.addFirst(new ArrayList<String>(this.emptySlots));
	}

	private void clearUndoLinkedLists()
	{
		this.boardUndoLinkedList.clear();
		this.scoreUndoLinkedList.clear();
		this.emptySlotsUndoLinkedList.clear();
	}

	@Override
	// Undo the last move
	public void undo() 
	{
		if (!this.boardUndoLinkedList.isEmpty())
		{
			int[][] tempBoard = boardUndoLinkedList.pollFirst();

			for (int i = 0; i < this.N; i++)
				for (int j = 0; j < this.N; j++)
					this.board[i][j] =  tempBoard[i][j];

			this.score = this.scoreUndoLinkedList.pollFirst();

			ArrayList<String> tempEmptySlots = emptySlotsUndoLinkedList.pollFirst();
			this.emptySlots.clear();
			this.emptySlots.addAll(tempEmptySlots);

			setChanged();
			notifyObservers();
		}
	}
	@Override
	//Mute
	public void mute()
	{
		if(getMuteFlag()==0)
		{
		bgMusic.stop();
		setMuteFlag(1);
		}
		else
		{
		bgMusic.play();
		setMuteFlag(0);
		}	
	}

	@Override
	// Return the amount of rows for the game (N)
	public int getRows() 
	{
		return this.N;
	}

	@Override
	// Return the amount of columns for the game (N)
	public int getCols() 
	{
		return this.N;
	}

	@Override
	public void save(String fileLocation) 
	{
		FileOutputStream fileOut = null;

		try
		{	
			fileOut = new FileOutputStream(fileLocation);

			String xml = xstream.toXML(this.CopyBeforeXML());

			fileOut.write(xml.getBytes());

			fileOut.close();
		} 
		catch (IOException e) { e.printStackTrace(); }
	}

	public Game2048Model CopyBeforeXML()
	{
		Game2048Model newModel = new Game2048Model();
		newModel.board = this.board;
		newModel.boardUndoLinkedList = this.boardUndoLinkedList;
		newModel.N = this.N;
		newModel.score = this.score;
		newModel.scoreUndoLinkedList = this.scoreUndoLinkedList;
		newModel.emptySlots = this.emptySlots;
		newModel.emptySlotsUndoLinkedList = this.emptySlotsUndoLinkedList;

		return newModel;
	}

	@Override
	public void load(String fileLocation) 
	{		
		try
		{
			Game2048Model newModel = (Game2048Model) xstream.fromXML(new FileReader(fileLocation));
			this.board = newModel.getBoard();
			this.score = newModel.getScore();
			this.boardUndoLinkedList = newModel.boardUndoLinkedList;
			this.scoreUndoLinkedList = newModel.scoreUndoLinkedList;
			this.N = newModel.getN();
			this.emptySlots = newModel.emptySlots;
			this.emptySlotsUndoLinkedList = newModel.emptySlotsUndoLinkedList;

		}
		catch (IOException e) { e.printStackTrace(); }

		setChanged();
		notifyObservers();
	}
	
	
	
	private int getN()
	{
		return this.N;
	}

	private void setN(int newN)
	{
		this.N = newN;;
	}

	public int[][] getBoard() 
	{
		return board;
	}

	public void setBoard(int[][] board) 
	{
		this.board = board;
	}

	public void setScore(int score) {
		this.score = score;
	}

	@Override
	public String toString()
	{
		String str = new String();

		for (int i = 0; i < this.N; i++)
		{
			for (int j = 0; j < this.N; j++)
			{
				str = str.concat(Integer.toString(this.board[i][j]));
				str = str.concat("\t");
			}
			str = str.concat("\n");
		}

		return str;
	}

	public int getMuteFlag() {
		return muteFlag;
	}

	public int setMuteFlag(int muteFlag) {
		this.muteFlag = muteFlag;
		return muteFlag;
	}

	@Override
	public void moveUpRight() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void moveUpLeft() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void moveDownLeft() {
		// TODO Auto-generated method stub
		
	}

	@Override
	public void moveDownRight() {
		// TODO Auto-generated method stub
		
	}
	
	public int connectToServer() {
		Message messageFromServer=null;
		State state = new State(getBoard());
		
		try{
			Socket myServer = new Socket(InetAddress.getLocalHost(),5000); //Uses Localhost and port 5000, if the computers are on different networks, then replace localhost with an IP address.
			
			ObjectOutputStream out2server = new ObjectOutputStream(myServer.getOutputStream());
			out2server.writeObject(new Message(state, "getHint", 0, "2048")); //Request for hint message
			
			ObjectInputStream inFromServer = new ObjectInputStream(myServer.getInputStream());
			messageFromServer = (Message) inFromServer.readObject();
			
			out2server.writeObject(new Message(null, "exit", 0, null)); //Closing client
			myServer.close(); //Closing socket
			return (messageFromServer.getDirection());
						
		} catch (IOException | ClassNotFoundException e) {
			System.out.println("server not found");
			e.printStackTrace();
		}
		return 0;
	}

	@Override
	public void changeSong() {
		//
		if(muteFlag != 1)
		{
			this.bgMusic.stop();
			this.bgMusic = new AudioPlayer("/sounds/dadada.mp3");
			bgMusic.play();
		}
		
	}
	
}


